""" These are basic flash commnads for the flag shell """
# Michael Cohen <scudette@users.sourceforge.net>
# David Collett <daveco@users.sourceforge.net>
#
# ******************************************************
#  Version: FLAG $Version: 0.87-pre1 Date: Thu Jun 12 00:48:38 EST 2008$
# ******************************************************
#
# * This program is free software; you can redistribute it and/or
# * modify it under the terms of the GNU General Public License
# * as published by the Free Software Foundation; either version 2
# * of the License, or (at your option) any later version.
# *
# * This program is distributed in the hope that it will be useful,
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# * GNU General Public License for more details.
# *
# * You should have received a copy of the GNU General Public License
# * along with this program; if not, write to the Free Software
# * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
# ******************************************************
import pyflag.pyflagsh as pyflagsh
import sys,os,posixpath, pdb
import pyflag.FlagFramework as FlagFramework
import pyflag.DB as DB
import pyflag.IO as IO
import pyflag.FileSystem as FileSystem
import time
import pyflag.Registry as Registry
import pyflag.pyflaglog as pyflaglog
import pyflag.conf
config=pyflag.conf.ConfObject()
import fnmatch
import pyflag.TEXTUI as TEXTUI
import pyflag.CacheManager as CacheManager
import pyaff4

oracle = pyaff4.Resolver()

class load(pyflagsh.command):
    """ Assigns a current case for use in the shell """
    def help(self):
        return  "load case.iosource: loads the case into the shell."
    
    def execute(self):
        args=self.args
        text=''
        try:
            case=args[0]
            dbh = DB.DBO(case)

            self.environment._FS=FileSystem.DBFS(case)
            self.environment._CASE = case
            yield "Loaded case %r" %(case)
        except Exception,e:
            raise RuntimeError("Unable to open filesystem %s (%s)" %(text,e))

    def complete(self,text,state):
        """ Completes the command for the different filesystems """
        args=self.args
        dbh=DB.DBO()
        dbh.execute("select value from meta where property=%r","flag_db")
        cases=[ row['value'] for row in dbh ]
        for i in range(state,len(cases)):
            if cases[i].startswith(text):
                return cases[i]

class ls(pyflagsh.command):
    """ Implement the ls command. """
    optlist="lR"
    def help(self):
        return "ls [dir]:  lists the files in the current directory (if dir not specified) or in dir."

    def execute(self):
        args=self.args
        if len(args)==0:
            args.append(self.environment.CWD)

        for path in self.glob_files(args):
            for f in self.list(path):
                yield f
            
    def list(self,path):
        """ List the files in a particular path """
        path=posixpath.abspath(posixpath.join(self.environment.CWD,path))
        yield "%s\t%s\t%8s\t%s" % ('mode',
                                  'size',
                                  'inode_id',
                                  'name')

        try:
            if self.opts.has_key('-l'):
                for dir in self.environment._FS.longls(path=path):
                    if dir['name']:
                        yield "%s\t%s\t%8d\t%s" % (dir['mode'],
                                                  dir.get('size','-'),
                                                  dir['inode_id'] or 0,
                                                  dir['name'])

            else:
                for dir in self.environment._FS.ls(path=path,dirs=1):
                    ## Remove the current dir from path:
                    if path.startswith(self.environment.CWD):
                        new_path=path[len(self.environment.CWD):]
                    else: new_path=path
                    
                    if dir:
                        #yield "[%s/%s]" % (new_path,dir)
                        yield "[%s]" % (dir)

                for dent in self.environment._FS.longls(path=path,dirs=0):
                    if dent:
                        #yield " %s/%s " % (dent['path'],dent['name'])
                        yield " %s " % (dent['name'])

            ## Do we need to recurse?
            if self.opts.has_key('-R'):
                for dir in self.environment._FS.longls(path=path,dirs=1):
                    if dir['name']:
                        new_path = path + dir['name']
                        yield "Directory %s:" % (new_path)
                        for file in self.list(new_path):
                            yield file

        except AttributeError:
            raise RuntimeError("No Filesystem loaded, do you need to load a filesystem first?")

    def complete(self,text,state):
        args=self.args
        if len(args)==1: args.append('.')
        path,name=posixpath.split(args[-1])
        path=posixpath.abspath(posixpath.join(self.environment.CWD,path))

        ## This does an ls of the current directory
        files=[ file for file in self.environment._FS.ls(path=path,dirs=1)]
        files.extend([ file for file in self.environment._FS.ls(path=path,dirs=0)])
        for i in range(state,len(files)):
            if files[i].startswith(text):
                return files[i]

class cd(ls):
    """ Change working directory """
    def help(self):
        return  "cd [dir]: changes directory to root (if dir not specified) or to dir."
    
    def execute(self):
        args=self.args
        try:
            path=args[0]
        except IndexError:
            path="/"

        new_path=posixpath.abspath(posixpath.join(self.environment.CWD,path))
        if not new_path.endswith('/'):
            new_path+='/'

        path=FlagFramework.normpath(new_path)
        if new_path!='/':
        ## Now check if the new path actually exists (There is an edge case here with / does have an inode):
            if not self.environment._FS.isdir(new_path):
                raise RuntimeError("No such directory: %s" % new_path)
        
        self.environment.CWD=new_path
        yield 'current working directory %s' % self.environment.CWD

    def complete(self,text,state):
        args=self.args
        if len(args)==1: args.append('.')
        path,name=posixpath.split(args[-1])
        path=posixpath.abspath(posixpath.join(self.environment.CWD,path))

        ## This does an ls of the current directory only for directories.
        files=[ file for file in self.environment._FS.ls(path=path,dirs=1)]
        for i in range(state,len(files)):
            if files[i].startswith(text):
                return files[i]

class cat(ls):
    """ Dumps the content of the file """
    def execute(self):
        args = self.args
        
        for arg in self.glob_files(args):
            fd=self.environment._FS.open(arg)
            while 1:
                f=fd.read(1000000)
                if len(f)>0:
                    yield f
                else: break

class less(ls):
        """ Pipes the content of the file to less """
        def help(self):
            return "Pipe files to less pager "

        def execute(self):
            pager = os.environ.get("PAGER","less")
            for arg in self.args:
                arg = os.path.join(self.environment.CWD, arg)
                fd=self.environment._FS.open(path=arg)
                pipe=os.popen(pager,"w")

                while 1:
                    data=fd.read(10000)
                    if not data: break
                    pipe.write(data)

                pipe.close()
                yield 'Viewing of %s with less successful' % arg


class cp(ls):
    """ Copies files from the filesystem to the directory specified as the last arg """
    def help(self):
        return "cp files dest: copies files (which could use globs) to destination directory (on the real system).\n(note: This will not overwrite a file!)"
    
    def execute(self):
        args=self.args
        target=args[-1]
        #Check to see if the target is a valid directory:
        if not posixpath.isdir(target):
            raise IOError("Target %s is not a directory. (Note: Target must exist on the host filesystem)")
        
        for arg in self.glob_files(args[:-1]):
            ## FIXME: implement a -R switch
            #target_path=target + '/' + arg[len(self.environment.CWD):]
            target_path=target + '/' + posixpath.basename(arg)
            outfd=open(target_path,"w")
            try:
                fd=self.environment._FS.open(arg)
                while 1:
                    f=fd.read(1000000)
                    if not f: break
                    outfd.write(f)

                yield "Copied %s in image to %s on host" % (arg,target_path)
            except IOError,e:
                yield "Unable to copy %s: %s" %(arg,e)

class help(pyflagsh.command):
    """ Print some help about a command (try help help) """
    def help(self):
        return("""PyFlag shell allows direct access to the filesystems. Command line expansion is supported. The following commands are defined, type help command to find out more:
%s
""" % Registry.SHELL_COMMANDS.commands.keys())
    
    def complete(self,text,state):
        commands = Registry.SHELL_COMMANDS.commands.keys()
        for i in range(state,len(commands)):
            if commands[i].startswith(text):
                return commands[i]
            
    def execute(self):
        args=self.args
        if len(args)==0:
            args.append('help')

        for i in args:
            command=Registry.SHELL_COMMANDS[i]([],self.environment)
            try:
                yield(command.help())
            except pyflagsh.ParserException:
                yield(command.__doc__)

class pwd(pyflagsh.command):
    """ Print the current working directory """
    def execute(self):
        yield "Current directory is %s" % self.environment.CWD

class set(pyflagsh.command):
    """ Sets and reads different values in the environment """
    def complete(self,text,state):
        env = [ i for i in dir(self.environment) if not i.startswith('_') ]
        for i in range(state,len(env)):
            if env[i].startswith(text):
                return env[i]
            
    def execute(self):
        args=self.args
        if len(args)==0:
            for i in dir(self.environment):
                if not i.startswith('_'):
                    try:
                        yield "%s = %s" %(i,self.environment.__dict__[i])
                    except KeyError:
                        pass
        else:
            for i in args:
                try:
                    index=i.index("=")
                    self.environment.__dict__[i[:index]]=i[index+1:]
                except ValueError:
                    yield self.environment.__dict__[i]
class quit(pyflagsh.command):
    """ Exits the shell"""
    def help(self):
        return "quit: Exits the PyFlag shell"

    def execute(self):
        sys.exit()
        
class exit(pyflagsh.command):
    """ Exits the shell"""
    def help(self):
        return "exit: Exits the PyFlag shell"
    
    def execute(self):
        sys.exit()

class fstat(pyflagsh.command):
    """ stats an file in the filesystem """
    def execute(self):
        args=self.args
        for arg in args:
            path, name = os.path.split(arg)
            ## Glob the inodes:
            dbh = DB.DBO(self.environment._CASE)
            dbh.execute("select inode_id from vfs where path=%r and name=%r", path, name)
            for row in dbh:
                inode_id = row['inode_id']      
                status=self.environment._FS.istat(inode_id=inode_id)
                if not status:
                    raise RuntimeError("No status available for %s" % arg)

                yield status

class istat(pyflagsh.command):
    """ Stats an object in the VFS based on its inode id """
    def execute(self):
        for inode_id in self.args:
            status = self.environment._FS.istat(inode_id=inode_id)
            if not status:
                raise RuntimeError("No status available for %s" % inode_id)
            
            yield status

class iless(istat):
    """ Pipes the content of an inode to less """
    def help(self):
        return "Pipe inodes to less pager "
        
    def execute(self):
        pager = os.environ.get("PAGER","less")
        for inode in self.args:
            fd=self.environment._FS.open(inode=inode)
            pipe=os.popen(pager,"w")
            while 1:
                data=fd.read(1000000)
                if not data: break
                pipe.write(data)
                
            pipe.close()
            yield 'Viewing of %s with less successful' % inode

class iiless(iless):
    def help(self):
        return """ Dump the value of an inode_id (inode ids are internal db ids for the inodes, this is probably not generally useful for anyone other than developers) """
    
    def execute(self):
        if not self.environment._CASE:
            raise RuntimeError("You must specify a case first (try load casename)")
        dbh = DB.DBO(self.environment._CASE)
        pager = os.environ.get("PAGER","less")
        for inode_id in self.args:
            fd=self.environment._FS.open(inode_id=inode_id)
            pipe=os.popen(pager,"w")
            while 1:
                data=fd.read(10000)
                if not data: break
                pipe.write(data)
                
            pipe.close()
            yield 'Viewing of inode_id %s with less successful' % inode_id


    
class icp(iless):
    """ Copy Inodes from the VFS to the file system """
    def execute(self):
        ## check that last arg is a dir
        mode = "file"
        if len(self.args)>2 and not os.isdir(self.args[-1]):
            raise RuntimeError("Last argument must be a directory for multiple files")
        else:
            mode = "directory"
        
        for inode in self.args[:-1]:
            fd=self.environment._FS.open(inode=inode)
            if mode =='directory':
                output_filename = inode.replace("/","_")
                outfd = open("%s/%s" % (self.args[-1], output_filename),'w')
            else:
                outfd = open(self.args[-1],'w')
                
            while 1:
                data=fd.read(10000)
                if not data: break
                outfd.write(data)
                    
            outfd.close()
            yield 'Copying of %s into %s successful' % (inode,self.args[-1])

    
class iicp(iless):
    """ Copy Inodes from the VFS to the file system """
    def execute(self):
        ## check that last arg is a dir
        mode = "file"
        if len(self.args)>2 and not os.isdir(self.args[-1]):
            raise RuntimeError("Last argument must be a directory for multiple files")
        else:
            mode = "directory"
        
        for inode_id in self.args[:-1]:
            fd=self.environment._FS.open(inode_id=inode_id)
            if mode =='directory':
                output_filename = inode_id
                outfd = open("%s/%s" % (self.args[-1], output_filename),'w')
            else:
                outfd = open(self.args[-1],'w')
                
            while 1:
                data=fd.read(100000)
                if not data: break
                outfd.write(data)
                    
            outfd.close()
            yield 'Copying of %s into %s successful' % (inode_id,self.args[-1])

class stat(ls):
    """ stats a urn in the filesystem """
    def execute(self):
        for arg in self.args:
            yield oracle.export_dict(arg)
            
class execute(pyflagsh.command):
    """ Executes a report's analysis method with the required parameters """
    def help(self):
        return """
        This command executes a flag report giving it the arguments given. The general format of this command is:

        execute Family.ReportName arg1=value arg2=value

        Note that environment values are automatically included into the set of args. So you may use set to set args that are commonly used.
        Note also that command line completion is enabled for this, and so may be used liberally to assist with both the selection of reports and the args needed
        """
    def complete(self,text,state):
        args=self.args

        possibilities=[]
        allreports=[]
        families = Registry.REPORTS.get_families()
        for family in families:
            reports=Registry.REPORTS.family[family]
            for report in reports:
                possibilities.append("%s.%s" % (family,report.name))
                allreports.append(report)
        
        if len(args)<2 or len(args)==2 and text:
            for i in range(state,len(possibilities)):
                if possibilities[i].startswith(text):
                    return possibilities[i]
        else:
            for i in range(0,len(possibilities)):
                if possibilities[i] == args[1]:
                    r = allreports[i]
                    params = r.parameters.keys()
                    params.append('case')
                    for i in range(state,len(params)):
                        if params[i].startswith(text):
                            return params[i]

    def prepare(self):
        """ Returns a report, query all ready from the current args """
        args=self.args
        query=FlagFramework.query_type(())
 
        try:
            query['family'],query['report']=args[0].split('.')
        except:
            raise RuntimeError("Unable to parse %s as a family.report" % args[0])
        
        report = Registry.REPORTS.dispatch(query['family'],query['report'])
        ## Include the report and family:

        for arg in args[1:]:
            try:
#                del query[arg[:arg.index('=')]]
                query[arg[:arg.index('=')]]=arg[arg.index('=')+1:]
            except ValueError:
                raise RuntimeError("Argument should be of the form key=value, got %s" % arg)

        ## Include environment variables in the query:
        for arg in dir(self.environment):
            if not arg.startswith('_') and not query.has_key(arg):
                try:
                    query[arg]=self.environment.__dict__[arg]
                except KeyError:
                    pass

#        if not query.has_key('case'): query['case']=config.FLAGDB
        return report,query

    def execute(self):
        start_time=time.time()
        report,query = self.prepare()

        pyflaglog.log(pyflaglog.VERBOSE_DEBUG, "Flash will execute the following query: %s" % query)

        ## Instantiate the report
        report=report(self.environment._flag)
        if self.environment._flag.is_cached(query):
            ## Run the display method
            result=TEXTUI.TEXTUI(query=query)
            report.display(query,result)
            yield result.display()
            return
        
        ## Execute the report:
        try:
            report.analyse(query)
            try:
                dbh = DB.DBO(query['case'])
            except KeyError:
                dbh = DB.DBO()
                
            canonical_query = FlagFramework.canonicalise(query)
            ## We call the display method just in case this report
            ## does something in the display
            result=TEXTUI.TEXTUI(query=query)
            report.display(query,result)
            yield result.display()
            dbh.execute("insert into meta set property=%r,value=%r",('report_executed',canonical_query))
            yield "Execution of %s successful in %s sec" % (self.args[1],time.time()-start_time)
            pyflaglog.log(pyflaglog.VERBOSE_DEBUG, "Flash successfully ran the following query: %s" % query)
        except Exception,e:
            pyflaglog.log(pyflaglog.WARNING, "Flash encountered the following error: %s when running query: %s" % (e,query))
            print FlagFramework.get_bt_string(e)
            raise
            raise RuntimeError("%s: %s after %s sec" %  (sys.exc_info()[0],sys.exc_info()[1],time.time()-start_time))

class reset(execute):
    """ Resets the given report """
    def help(self):
        return """
        reset report: This command resets a flag report. After running this command the state of the database should be returned to what it was before the report was executed.
        """
    def execute(self):
        if len(self.args) < 1:
            yield self.help()
            return      

        start_time=time.time()
        report,query = self.prepare()

        ## Instantiate the report:
        report=report(self.environment._flag)
        
        ## Execute the report:
        try:
            report.do_reset(query)
            yield "Resetting of %s successful in %s sec" % (self.args[0],time.time()-start_time)            
        except Exception,e:
            import traceback
            print traceback.print_tb(sys.exc_info()[2])
            raise RuntimeError("%s: %s after %s sec" %  (sys.exc_info()[0],sys.exc_info()[1]),time.time()-start_time)

class find(ls):
    """ A command to find files in the filesystem """
    long_opts = [ "name=", "type=" ]
    
    def execute(self):
        for path in self.args:
            for file in self.list(path):
                yield posixpath.normpath( "/////%s/%s" % (file['path'],file['name']))
        
    def list(self,path):
        """ List the files in a particular path """
        path=posixpath.abspath(posixpath.join(self.environment.CWD,path))
        try:
            if self.environment._FS.isdir(path):
                if not path.endswith('/'):
                    path=path+'/'

                for dir in self.environment._FS.longls(path=path, dirs=0):
                    yield dir

                for dir in self.environment._FS.longls(path=path, dirs=1):
                    yield dir
                    for file in self.list(path+dir['name']):
                        yield file

#            else:
#                yield {'name':path, 'path':''}

        except AttributeError:
            raise RuntimeError("No Filesystem loaded, do you need to load a filesystem first?")

class find_dict(find):
    """ This command returns a full dict of information for each file returned """
    def execute(self):
        for path in self.args[1:]:
            return self.list(path)

class file(ls):
    """ Returns the file magic of args """
    def execute(self):
        #Find the inode of the file:
        import pyflag.Magic as Magic

        m = Magic.MagicResolver()
        for path in self.args:
            type, mime = m.find_inode_magic(case = self.environment._CASE, urn = path)

            yield dict(type=type, mime = mime)

class seal(load):
    """ Seals the specified case by closing off the AFF4 volume. After using this command its possible to use the AFF4 volume (in the results dir) to interchange data.
    """
    def execute(self):
        args=self.args
        text=''
        try:
            case=args[0]
            yield "Completing AFF4 volume (This could take a while)"
            CacheManager.AFF4_MANAGER.close(case)
            yield "Done"
        except Exception,e:
            raise RuntimeError("Unable to open filesystem %s (%s)" %(text,e))


class ifile(ls):
    """ Returns the file magic of args """
    def execute(self):
        #Find the inode of the file:
        import pyflag.Magic as Magic

        m = Magic.MagicResolver()
        for inode_id in self.args:
            type, mime, scores = m.find_inode_magic(
                case = self.environment._CASE, inode_id=inode_id)
            
            yield scores
            yield dict(type=type, mime = mime)

class delete_case(load):
    """ Drop a case and delete all its data """
    
    def help(self):
        return """Delete the specified case and all data within it. E.g. 'delete_case foobar'"""

    def execute(self):
        try:
            case=self.args[0]
            dbh = DB.DBO(case)
            FlagFramework.delete_case(case)
            yield "Deleted case %r" %(case)
        except Exception,e:
            ## Should we just return here or report an error?
            return
            raise RuntimeError("Unable to delete case %s (%s)" %(case,e))

#class create_iosource(load):
#    def help(self):
#        return "create_iosource CaseName iosource_name URL. Creates the named IOSource in the specified case using the URL "
#
##    def execute(self):
##        case = self.args[0]
##        name = self.args[1]
##        url = self.args[2]
        

class create_case(load):
    """ Create a new PyFlag case """
    
    def help(self):
        return """Create a new empty case with specified name. E.g. 'create_case foobar'"""

    def execute(self):
        dbh = DB.DBO(None)
        case = self.args[0]
        dbh.cursor.ignore_warnings = True
        try:
           dbh.execute("Create database `%s` default character set utf8",(case))
        except DB.DBError, e:
           raise RuntimeError("Unable to create case %s, does the database "\
                              "already have a table with this name? Cowardly"\
                              " refusing to replace it. "\
                              " Error was %s" % (case, e))
           
        dbh.execute("select * from meta where property='flag_db' and value=%r",case)
        if not dbh.fetch():
            dbh.insert('meta',
                       property='flag_db',
                       value=case)

            ## Post the create event on the case
            FlagFramework.post_event('create', case)

            ## set any case parameters that were provided
            params = dict([arg.split("=",1) for arg in self.args[1:] if "=" in arg])

            ## add a default TZ if not present
            if not params.has_key("TZ"):
            	params["TZ"] = "SYSTEM"

            case_dbh  = DB.DBO(case)
            for p in params:
                case_dbh.insert("meta", property = p, value = params[p])
            
        yield "Created Case %s" % case
        
## Unit tests:
import unittest
from hashlib import md5
import pyflag.pyflagsh as pyflagsh
import pyflag.tests

class BasicCommandTests(pyflag.tests.ScannerTest):
    """ Test PyFlash commands """
    test_case = "PyFlag Test Case"
    test_file = "pyflag_stdimage_0.4.e01"
    subsystem = 'EWF'
    offset = "16128s"

    def test01ls(self):
        """ Test the ls command """
        self.env = pyflagsh.environment(case=self.test_case)
        pyflagsh.shell_execv(env=self.env, command="load",
                             argv=[self.test_case,])

        ## Check we can list default directory
        lines = [ l for l in pyflagsh.shell_execv_iter(env=self.env, command="ls",
                                                       argv=[])]
        self.assertEqual(len(lines),18)

        ## Check we can list directories
        lines = [ l for l in pyflagsh.shell_execv_iter(env=self.env, command="ls",
                                                       argv=["docs"])]
        self.assert_(len(lines)>=3)

        ## Check that we can glob files:
        lines = [ l for l in pyflagsh.shell_execv_iter(env=self.env, command="ls",
                                                       argv=["*.jpg"])]
        self.assertEqual(len(lines),5)
        
        ## Check that we can glob directories:
        lines = [ l for l in pyflagsh.shell_execv_iter(env=self.env, command="ls",
                                                       argv=["do*"])]
        self.assert_(len(lines)>3)

    def test02catTests(self):
        """ Test the cat command """
        self.env = pyflagsh.environment(case=self.test_case)
        pyflagsh.shell_execv(env=self.env, command="load",
                             argv=[self.test_case,])

        self.fsfd = FileSystem.DBFS(self.test_case)
        fd = self.fsfd.open("/dscf1080.jpg")
        data1=fd.read()        
        fd = self.fsfd.open("/dscf1081.jpg")
        data2=fd.read()
        fd = self.fsfd.open("/dscf1082.jpg")
        data3=fd.read()

        result = ''
        for l in pyflagsh.shell_execv_iter(env=self.env, command="cat",
                                           argv=["/dscf1081.jpg"]):
            result+=l
        self.assertEqual(result,data2)

        result = ''
        for l in pyflagsh.shell_execv_iter(env=self.env, command="cat",
                                           argv=["/dscf108*"]):
            result+=l

        self.assertEqual(len(result),len(data1)+len(data2)+len(data3))
        self.assert_(result==data1+data2+data3)

    def test03cpTests(self):
        """ Test the cp (copy) command """
        self.env = pyflagsh.environment(case=self.test_case)
        pyflagsh.shell_execv(env=self.env, command="load",
                             argv=[self.test_case,])

        ## Make a directory for the files:
        tmpname = os.tmpnam()
        os.mkdir(tmpname)

        pyflagsh.shell_execv(env=self.env, command="cp",
                             argv=["/dscf108*", tmpname])

        ## Now verify the copy worked:
        fd = open(tmpname+"/dscf1080.jpg",'r')
        data = fd.read()
        md5sum = md5.new()
        md5sum.update(data)
        self.assertEqual(md5sum.hexdigest(),'9e03e022404a945575b813ffb56fd841')

        ## Clean up:
        for file in os.listdir(tmpname):
            os.unlink(tmpname+'/'+file)
            
        os.rmdir(tmpname)
